Skip to content

Don't block the event loop on sync resource and prompt functions#2380

Merged
maxisbey merged 2 commits intomainfrom
fix/sync-resources-prompts-block-event-loop
Mar 31, 2026
Merged

Don't block the event loop on sync resource and prompt functions#2380
maxisbey merged 2 commits intomainfrom
fix/sync-resources-prompts-block-event-loop

Conversation

@maxisbey
Copy link
Copy Markdown
Contributor

Extends the #1909 fix to resources and prompts — sync @mcp.resource and @mcp.prompt handlers now run in worker threads instead of blocking the event loop.

Motivation and Context

#1909 fixed tools by routing sync functions through anyio.to_thread.run_sync, but the same blocking pattern existed in three other places:

  • FunctionResource.read()
  • ResourceTemplate.create_resource()
  • Prompt.render()

All three called self.fn(...) directly and checked inspect.iscoroutine(result) afterward. A blocking sync handler (file I/O, HTTP request, CPU-bound work) would freeze the entire event loop.

This applies the same fix: check inspect.iscoroutinefunction(self.fn) up front and dispatch sync functions to a worker thread.

Related: #1646, #1839

How Has This Been Tested?

Added thread-identity regression tests for each of the three call sites. Each test captures threading.get_ident() inside a sync handler and asserts it differs from the event loop's thread.

Verified that pydantic.validate_call (which wraps the stored self.fn in templates and prompts) preserves async-ness — inspect.iscoroutinefunction(validate_call(async_fn)) returns True, so the dispatch check works correctly on the wrapped function.

All 39 tests in the affected test files pass; pyright, ruff, and strict-no-cover are clean.

Breaking Changes

None. Sync handlers that were previously starving the event loop now run concurrently.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

The previous implementation happened to support callable objects with async __call__ (by checking iscoroutine(result) after calling). That edge case is not preserved here — matching the approach taken in #1909, which relies on _is_async_callable being evaluated at registration time for tools. Resources and prompts have no equivalent pre-computed field; if that edge case matters, it's worth a separate discussion.

AI Disclaimer

PR #1909 fixed this for tools by running sync functions via
anyio.to_thread.run_sync, but the same blocking pattern existed in
FunctionResource.read, ResourceTemplate.create_resource, and
Prompt.render. All three called self.fn() directly and checked
inspect.iscoroutine(result) afterward, so a blocking sync
@mcp.resource or @mcp.prompt handler would still freeze the event loop.

This applies the same fix: check inspect.iscoroutinefunction(self.fn)
up front and dispatch sync functions to a worker thread. Verified that
pydantic.validate_call (used to wrap stored functions in templates and
prompts) preserves async-ness, so the check works correctly on the
wrapped function.

Github-Issue: #1646
Proves the behavioral fix directly: the handler blocks on a
threading.Event in a worker thread while the async side awaits an
anyio.Event. The handler signals back into the event loop via
anyio.from_thread.run_sync, so the async side's await resolves
without polling or sleeps.

On regression (sync runs inline), anyio.from_thread.run_sync raises
RuntimeError immediately since there is no worker-thread context,
failing fast rather than waiting out the fail_after timeout.
@maxisbey maxisbey marked this pull request as ready for review March 31, 2026 17:27
Copy link
Copy Markdown
Member

@Kludex Kludex left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure those tests are useful. I would prefer to not have tests in this case.

@maxisbey maxisbey merged commit 3ce0f76 into main Mar 31, 2026
30 checks passed
@maxisbey maxisbey deleted the fix/sync-resources-prompts-block-event-loop branch March 31, 2026 17:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants